Add analog event type support with threshold-based detection#54
Merged
stevevanhooser merged 11 commits intomainfrom Apr 13, 2026
Merged
Add analog event type support with threshold-based detection#54stevevanhooser merged 11 commits intomainfrom
stevevanhooser merged 11 commits intomainfrom
Conversation
…X configs - Add analog event channel types (aep/aen/aimp/aimn) with threshold suffix support to daqsystemstring.py and mfdaq.py, mirroring MATLAB readevents_epochsamples for both live and ingested data paths - Add +Inf clamping to epochtimes2samples and epochtimes2samples_ingested so [-Inf, +Inf] maps to full epoch sample range - Add channeltype2str and parse_analog_event_channeltype static methods to daqsystemstring - Copy 5 new Neuropixels GLX DAQ system configs from NDI-matlab - Update bridge YAMLs with new sync hashes, methods, and decision logs for daqsystemstring (2157c70), mfdaq (9e11fbb), dataset (f485088) - Mark copy_session_to_dataset as deprecated in database_fun bridge (moved to ndi.dataset.copySessionToDataset in MATLAB) https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The nielsen_visnp DAQ system config references reader class ndi.setup.daq.reader.mfdaq.stimulus.nielsenvisneuropixelsglx which did not exist in Python, causing the kjnielsenlab blank session test to show 3 DAQ systems instead of 4. Creates a minimal stimulus reader class inheriting from the NDR reader, following the same pattern as nielsenvisintan. Registers it in the class registry so DAQ system loading succeeds. https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The vhajbpod_np config specifies a custom FileNavigatorClass (ndi.setup.file.navigator.vhlab_np_epochdir) which Python's lab.py was ignoring, always defaulting to ndi.file.navigator.epochdir. When loading MATLAB-generated artifacts, the DAQ system document contained the custom navigator class name, which Python couldn't resolve, causing the entire DAQ system to be silently dropped (7 vs 8 in the symmetry test). - Update lab.py to use FileNavigatorClass from JSON config when present - Register vhlab_np_epochdir in class_registry mapping to epochdir https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The ndr.py wrapper was incorrectly delegating all read operations to the SpikeInterface adapter, which defaults all channels to "analog_in" regardless of actual type. This meant digital_in, time, and other channel types were never properly reported. NDR-python already has fully implemented format-specific readers (e.g. ndr_reader_neuropixelsGLX) that correctly return channel types — but they were never called. Rewritten to match MATLAB's ndr.m pattern: - Store ndr_reader_string from document or constructor - Delegate to ndr.reader(ndr_reader_string) for all methods - Convert NDR dict output to ChannelInfo objects - Handle MightHaveTimeGaps for time-gap interpolation - Convert NDR ClockType to NDI ndi_time_clocktype https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
- Run black on ndr.py to fix formatting (line-length = 100) - Add CI lint & test commands section to AGENTS.md so agents know to run black, ruff, and pytest before pushing https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The ndr.py rewrite broke two symmetry tests where daqreader_ndr
documents had a default ndr_reader_string ("RHD") that didn't match
the actual data format (Axon ABF / Intan via SpikeInterface).
Also fixed a wrong relative import: 'from ...time' resolved to
'ndi.daq.time' (nonexistent) instead of 'ndi.time'. Changed to
absolute import 'from ndi.time'.
Each delegation method now tries NDR first, then falls back to
SpikeInterface on failure. This preserves the new NDR-native path
for configs that set ndr_reader_string correctly, while maintaining
backward compatibility with existing documents.
https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The Axon NDR symmetry test creates a daqreader_ndr document with the
default ndr_reader_string ("RHD") but Axon .abf data. CI doesn't have
spikeinterface installed, so the SpikeInterface fallback also fails.
Instead of relying on SpikeInterface as fallback, _get_ndr_reader now
accepts epochfiles and auto-detects the correct NDR reader from file
extensions (.abf -> axon_abf, .rhd -> intan_rhd, etc.) when the
stored reader string doesn't match. This removes the SpikeInterface
dependency for the common case and lets NDR handle all formats natively.
https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
ndr.reader("RHD") succeeds (valid alias for intan_rhd) but then
fails when asked to read .abf files. The previous fallback approach
only caught the ndr.reader() instantiation failure, not the later
read failure.
Fix: when epochfiles are available, always detect the reader from
file extensions first. This correctly maps .abf -> axon_abf before
the wrong default reader is ever used. Falls back to the stored
ndr_reader_string only when no epoch files are given or no extension
matches.
https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The Axon NDR symmetry test was missing the ndr_reader_string, relying on the template default "RHD". Fixed the test to explicitly set "axon_abf". Removed the file-extension auto-detection logic from ndr.py — the reader type should always be set explicitly when the DAQ system is created. https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The ndr.py rewrite now routes Axon ABF files through NDR's native axon_abf reader instead of SpikeInterface. NDR's axon_abf reader requires pyabf, which is in NDR's optional [formats] extra. Updated the NDR dependency to install with this extra. https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
When a custom navigator class (e.g. vhlab_np_epochdir) is mapped to a generic Python class (epochdir), the session summary was reporting the Python class constant instead of the document's actual class name. This caused a mismatch with MATLAB artifacts. After constructing the navigator from a document, override the instance's NDI_FILENAVIGATOR_CLASS to match the document value when they differ. https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds support for analog event channel types (aep, aen, aimp, aimn) that derive events from analog input channels by detecting threshold crossings. It includes threshold parsing, event detection logic, and infrastructure updates.
Key Changes
Core Functionality
is_analog_event_type()static method to identify and parse analog event channel types with optional threshold suffixes (e.g.,aep_t2.5)_read_analog_events()and_read_analog_events_ingested()methods that:readevents_epochsamples()andreadevents_epochsamples_ingested()to route analog event types to the new detection methodsUtility Functions
parse_analog_event_channeltype()to extract base type and threshold from strings likeaep_t2.5channeltype2str()to reconstruct device string segments with threshold suffixes in correct positionepochtimes2samples()andepochtimes2samples_ingested()to clamp positive infinity to the last sample of the epoch (previously only handled negative infinity)Configuration
nielsen_neuropixelsGLX.jsonvhneuropixelsGLX.jsonvhneuropixels.jsonvhajbpod_np.jsonnielsen_visnp.jsonDocumentation
ndi.database.fun.copy_session_to_datasetas deprecated (moved tondi.dataset.copySessionToDataset)Implementation Details
ai_data[:-1] < threshandai_data[1:] >= thresh(or inverse for negative crossings)https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ